package main;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import com.sun.opengl.util.GLUT;
import javax.vecmath.*;

import com.sun.opengl.util.texture.*;

import sceneGraph.*;
import utils.CubicMapping;

public class Renderer implements Visitor {
	private GLAutoDrawable drawable;
	private GLU glu;
	private GL gl;
	private GLUT glut;

	private int width;
	private int height;

	private Scene scene;

	public void setEnvironment(GLAutoDrawable drawable, GLU glu, GLUT glut, int width, int height) {
		this.drawable = drawable;
		this.glu = glu;
		this.glut = glut;
		this.width = width;
		this.height = height;

		gl = drawable.getGL();
	}

	@Override
	public void visit(Scene scene) {
		this.scene = scene;

		final GL gl = drawable.getGL();
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		// Update the camera inside the gl
		scene.getCamera().accept(this);
		scene.getSkyBox().accept(this);
		for (ISceneObject obj : scene.getPlanets()) {
			obj.accept(this);
		}

		for (TextObject textObj : scene.getTextObjects()) {
			textObj.accept(this);
		}
	}

	@Override
	public void visit(Camera camera) {
		// Initialize projection matrix
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();

		// Perspective
		float widthHeightRatio = (float) width / (float) height;
		glu.gluPerspective(45, widthHeightRatio, 1, 1000);

		// Position the camera
		Point3f pos;
		Vector3f upDirection;
		Point3f lookAt;

		if (camera.isMapMode()) {
			upDirection = camera.getUpDirection();
			lookAt = camera.getAbsLookAt();
			pos = camera.getAbsPosition();
		} else {
			lookAt = new Point3f(camera.getPlanet().getAbsPosition());
			lookAt.add(camera.getPlayer().getPosition());

			upDirection = new Vector3f();
			upDirection.cross(camera.getPlayer().getLeftDirection(), new Vector3f(camera.getPlayer().getPosition()));
			upDirection.normalize();

			// Set player position to be above the player, little backward
			pos = new Point3f();
			pos.add(camera.getPlayer().getPosition(), camera.getPlanet().getAbsPosition());

			Vector3f backwardVector = new Vector3f(upDirection);
			backwardVector.scale(-8.7f);
			pos.add(backwardVector);

			Vector3f aboveVector = new Vector3f(camera.getPlayer().getPosition());
			aboveVector.normalize();
			aboveVector.scale(camera.getPlayer().getInitDistance() * 1f);
			pos.add(aboveVector);
		}

		glu.gluLookAt(pos.getX(), pos.getY(), pos.getZ(), lookAt.getX(), lookAt.getY(), lookAt.getZ(), upDirection.getX(), upDirection.getY(),
				upDirection.getZ());

		// Change back to model view matrix
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();

	}

	public void visit(Planet planet) {
		handlePlanet(planet);
	}

	public void visit(Moon moon) {
		handlePlanet(moon);
	}

	public void handlePlanet(AbstractPlanet planet) {
		gl.glPushMatrix();

		gl.glLoadIdentity();

		// Set material properties.
		handleAbstractSceneObject(planet);

		Point3f pos = planet.getAbsPosition();

		// Position the planet
		gl.glTranslatef(pos.getX(), pos.getY(), pos.getZ());

		if (planet.isSun()) {

			Color3f emission = new Color3f(planet.getKD());
			emission.add(planet.getKS());
			emission.add(planet.getKA());
			emission.clampMax(1);

			float[] matEmissionSun = { emission.getX(), emission.getY(), emission.getZ() };
			gl.glMaterialfv(GL.GL_FRONT, GL.GL_EMISSION, matEmissionSun, 0);

			// Prepare light parameters.
			float SHINE_ALL_DIRECTIONS = 1;
			// float[] lightPos = { pos.getX(), pos.getY(), pos.getZ(), SHINE_ALL_DIRECTIONS };
			float[] lightPos = { 0, 0, 0, SHINE_ALL_DIRECTIONS };

			// Set light parameters.
			int glLightId = GL.GL_LIGHT0 + planet.getSunNumber();
			gl.glLightfv(glLightId, GL.GL_POSITION, lightPos, 0);
			gl.glLightfv(glLightId, GL.GL_DIFFUSE, matEmissionSun, 0);
			gl.glLightfv(glLightId, GL.GL_SPECULAR, matEmissionSun, 0);
			gl.glLightfv(glLightId, GL.GL_AMBIENT, matEmissionSun, 0);
			gl.glEnable(glLightId);
		}

		// Draw the planet mesh
		planet.getMesh().accept(this);

		for (Moon moon : planet.getMoons()) {
			moon.accept(this);
		}

		/* Render only for current planet */
		if (planet.getPlayer() != null) {
			handleAbstractPlanetObject(planet.getPlayer(), planet);
			planet.getPlayer().accept(this);

			for (AbstractPlanetObject planetObject : planet.getPlanetObjects()) {
				handleAbstractPlanetObject(planetObject, planet);
				planetObject.accept(this);
			}

			// Enable all lights again (could have been disabled because of handleAbstractPlanetObject
			for (AbstractPlanet sun : scene.getPlanetsAndMoons()) {
				if (sun.isSun()) {
					int glLightId = GL.GL_LIGHT0 + sun.getSunNumber();
					gl.glEnable(glLightId);
				}
			}

		}

		gl.glPopMatrix();
	}

	private void handleAbstractSceneObject(AbstractSceneObject abstractSceneObject) {
		// Set material properties.
		float[] kDiffuse = { abstractSceneObject.getKD().getX(), abstractSceneObject.getKD().getY(), abstractSceneObject.getKD().getZ() };
		float[] kSpecular = { abstractSceneObject.getKS().getX(), abstractSceneObject.getKS().getY(), abstractSceneObject.getKS().getZ() };
		float[] kAmbient = { abstractSceneObject.getKA().getX(), abstractSceneObject.getKA().getY(), abstractSceneObject.getKA().getZ() };
		float[] matEmission = { 0, 0, 0 };
		float nShineness = abstractSceneObject.getNS();

		gl.glMaterialfv(GL.GL_FRONT, GL.GL_DIFFUSE, kDiffuse, 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, kSpecular, 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, kAmbient, 0);
		gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, nShineness);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_EMISSION, matEmission, 0);
	}

	public void visit(AbstractPlanetObject planetObject) {
		gl.glPushMatrix();

		// Set material properties.
		handleAbstractSceneObject(planetObject);

		// Position the object
		Point3f pos = planetObject.getPosition();
		Vector3f left = planetObject.getLeftDirection();
		Vector3f up = planetObject.getUpDirection();
		Vector3f dir = planetObject.getDirection();
		float[] matrix = { left.x, left.y, left.z, 0f, up.x, up.y, up.z, 0f, dir.x, dir.y, dir.z, 0.0f, pos.x, pos.y, pos.z, 1f };
		gl.glMultMatrixf(matrix, 0);

		if (planetObject.getMesh() != null) {
			planetObject.getMesh().accept(this);
		} else {
			GLUquadric quadric = glu.gluNewQuadric();

			Texture texture = planetObject.getTexture();
			if (texture != null) {
				texture.enable();
				texture.bind();
				glu.gluQuadricTexture(quadric, true);
			}

			final int slices = 20;
			final int stacks = 20;
			glu.gluQuadricDrawStyle(quadric, GLU.GLU_FILL);
			glu.gluQuadricNormals(quadric, GLU.GLU_FLAT);
			glu.gluQuadricOrientation(quadric, GLU.GLU_OUTSIDE);

			// Create the player
			glu.gluSphere(quadric, planetObject.getRadius(), slices, stacks);
			glu.gluDeleteQuadric(quadric);

			if (texture != null) {
				texture.disable();
			}
		}

		gl.glPopMatrix();
	}

	private void handleAbstractPlanetObject(AbstractPlanetObject obj, AbstractPlanet planet) {
		Point3f objAbsPosition = new Point3f();
		objAbsPosition.add(obj.getPosition(), planet.getAbsPosition());

		Vector3f planetToObj = new Vector3f();
		planetToObj.sub(objAbsPosition, planet.getAbsPosition());

		for (AbstractPlanet sun : scene.getPlanetsAndMoons()) {
			if (sun.isSun()) {
				Vector3f planetToSun = new Vector3f();
				planetToSun.sub(sun.getAbsPosition(), planet.getAbsPosition());

				int glLightId = GL.GL_LIGHT0 + sun.getSunNumber();
				if (planetToSun.dot(planetToObj) < 0) {
					gl.glDisable(glLightId);
				} else {
					gl.glEnable(glLightId);
				}
			}
		}
	}

	public void visit(PlanetMesh mesh) {

		Texture texture = mesh.getTexture();

		texture.enable();
		texture.bind();

		for (PlanetTriangle t : mesh.getFaces()) {
			t.accept(this);
		}

		texture.disable();

	}

	public void visit(PlanetTriangle triangle) {
		// gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE); // Wireframe

		Point2f params;
		gl.glBegin(GL.GL_TRIANGLES);

		Point3f vertex1 = triangle.getVertex1();
		Vector3f normal1 = triangle.getNormal1();
		params = CubicMapping.getParameterization(vertex1);
		gl.glNormal3f(normal1.getX(), normal1.getY(), normal1.getZ());
		gl.glTexCoord2f((params.x + 1) / 2, (params.y + 1) / 2);
		gl.glVertex3f(vertex1.getX(), vertex1.getY(), vertex1.getZ());

		Point3f vertex2 = triangle.getVertex2();
		Vector3f normal2 = triangle.getNormal2();
		params = CubicMapping.getParameterization(vertex2);
		gl.glNormal3f(normal2.getX(), normal2.getY(), normal2.getZ());
		gl.glTexCoord2f((params.x + 1) / 2, (params.y + 1) / 2);
		gl.glVertex3f(vertex2.getX(), vertex2.getY(), vertex2.getZ());

		Point3f vertex3 = triangle.getVertex3();
		Vector3f normal3 = triangle.getNormal3();
		params = CubicMapping.getParameterization(vertex3);
		gl.glNormal3f(normal3.getX(), normal3.getY(), normal3.getZ());
		gl.glTexCoord2f((params.x + 1) / 2, (params.y + 1) / 2);
		gl.glVertex3f(vertex3.getX(), vertex3.getY(), vertex3.getZ());

		gl.glEnd();
	}

	public void visit(ObjectMesh mesh) {

		gl.glBegin(GL.GL_TRIANGLES);
		for (ObjectTriangle t : mesh.getTriangles()) {
			Point3f vertex1 = new Point3f(t.getVertex1());
			Vector3f normal1 = t.getNormal1();
			gl.glNormal3f(normal1.getX(), normal1.getY(), normal1.getZ());
			gl.glVertex3f(vertex1.getX(), vertex1.getY(), vertex1.getZ());

			Point3f vertex2 = new Point3f(t.getVertex2());
			Vector3f normal2 = t.getNormal2();
			gl.glNormal3f(normal2.getX(), normal2.getY(), normal2.getZ());
			gl.glVertex3f(vertex2.getX(), vertex2.getY(), vertex2.getZ());

			Point3f vertex3 = new Point3f(t.getVertex3());
			Vector3f normal3 = t.getNormal3();
			gl.glNormal3f(normal3.getX(), normal3.getY(), normal3.getZ());
			gl.glVertex3f(vertex3.getX(), vertex3.getY(), vertex3.getZ());
		}
		gl.glEnd();
	}

	@Override
	public void visit(SkyBox skyBox) {
		// Store the current matrix
		gl.glPushMatrix();

		Texture texture;
		float factor = skyBox.getFactor();

		gl.glLoadIdentity();
		gl.glTranslatef(skyBox.getCamera().getAbsPosition().getX(), skyBox.getCamera().getAbsPosition().getY(), skyBox.getCamera().getAbsPosition().getZ());

		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP);

		// Enable/Disable features
		gl.glPushAttrib(GL.GL_ENABLE_BIT);
		gl.glEnable(GL.GL_TEXTURE_2D);
		gl.glDisable(GL.GL_DEPTH_TEST);
		gl.glDisable(GL.GL_LIGHTING);
		gl.glDisable(GL.GL_BLEND);

		// Just in case we set all vertices to white.
		gl.glColor3f(1f, 1f, 1f);

		// Render the front quad
		texture = skyBox.getFrontTexture();
		texture.enable();
		texture.bind();

		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0, 0);
		gl.glVertex3f(0.5f * factor, -0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(1, 0);
		gl.glVertex3f(-0.5f * factor, -0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(1, 1);
		gl.glVertex3f(-0.5f * factor, 0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(0, 1);
		gl.glVertex3f(0.5f * factor, 0.5f * factor, -0.5f * factor);
		gl.glEnd();

		texture.disable();

		// Render the left quad
		texture = skyBox.getLeftTexture();
		texture.enable();
		texture.bind();

		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0, 0);
		gl.glVertex3f(0.5f * factor, -0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 0);
		gl.glVertex3f(0.5f * factor, -0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(1, 1);
		gl.glVertex3f(0.5f * factor, 0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(0, 1);
		gl.glVertex3f(0.5f * factor, 0.5f * factor, 0.5f * factor);
		gl.glEnd();

		texture.disable();

		// Render the back quad
		texture = skyBox.getBackTexture();
		texture.enable();
		texture.bind();

		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0, 0);
		gl.glVertex3f(-0.5f * factor, -0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 0);
		gl.glVertex3f(0.5f * factor, -0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 1);
		gl.glVertex3f(0.5f * factor, 0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(0, 1);
		gl.glVertex3f(-0.5f * factor, 0.5f * factor, 0.5f * factor);

		gl.glEnd();

		texture.disable();

		// Render the right quad
		texture = skyBox.getRightTexture();
		texture.enable();
		texture.bind();

		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0, 0);
		gl.glVertex3f(-0.5f * factor, -0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(1, 0);
		gl.glVertex3f(-0.5f * factor, -0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 1);
		gl.glVertex3f(-0.5f * factor, 0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(0, 1);
		gl.glVertex3f(-0.5f * factor, 0.5f * factor, -0.5f * factor);
		gl.glEnd();

		texture.disable();

		// Render the top quad
		texture = skyBox.getTopTexture();
		texture.enable();
		texture.bind();

		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0, 1);
		gl.glVertex3f(-0.5f * factor, 0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(0, 0);
		gl.glVertex3f(-0.5f * factor, 0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 0);
		gl.glVertex3f(0.5f * factor, 0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 1);
		gl.glVertex3f(0.5f * factor, 0.5f * factor, -0.5f * factor);
		gl.glEnd();

		texture.disable();

		// Render the bottom quad
		texture = skyBox.getBottomTexture();
		texture.enable();
		texture.bind();

		gl.glBegin(GL.GL_QUADS);
		gl.glTexCoord2f(0, 0);
		gl.glVertex3f(-0.5f * factor, -0.5f * factor, -0.5f * factor);
		gl.glTexCoord2f(0, 1);
		gl.glVertex3f(-0.5f * factor, -0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 1);
		gl.glVertex3f(0.5f * factor, -0.5f * factor, 0.5f * factor);
		gl.glTexCoord2f(1, 0);
		gl.glVertex3f(0.5f * factor, -0.5f * factor, -0.5f * factor);
		gl.glEnd();

		texture.disable();

		// Restore enable bits and matrix
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glEnable(GL.GL_LIGHTING);
		gl.glEnable(GL.GL_BLEND);
		gl.glPopAttrib();

		gl.glPopMatrix();

	}

	@Override
	public void visit(TextObject textObj) {
		Point2f pos = textObj.getPosition();
		String text = textObj.getText();
		Color3f color = textObj.getColor();

		gl.glDisable(GL.GL_LIGHTING);

		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		glu.gluOrtho2D(0.0, width - 1, 0.0, height - 1);

		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();

		gl.glColor3f(color.x, color.y, color.z);
		gl.glRasterPos2f(pos.x, pos.y);

		glut.glutBitmapString(GLUT.BITMAP_HELVETICA_18, text);

		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glPopMatrix();

		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPopMatrix();

		gl.glEnable(GL.GL_LIGHTING);
	}
}